package semantic.analyzer;

import java.util.LinkedList;
import java.util.List;

import metamodel.Accessorizable;
import metamodel.Attribute;
import metamodel.MetamodelManager;
import metamodel.Method;
import metamodel.MyClass;
import metamodel.PrimitiveTypeEnum;
import semantic.SemanticException;
import semantic.analyzer.StereotypeAnalyzer.StereotypeTypeEnum;
import semantic.model.AccessorTypeEnum;
import semantic.model.Context;
import semantic.model.ExpressionNode;
import semantic.model.PrimaryExpressionNode;
import semantic.model.PropertyCallNode;
import semantic.model.UndefinedExpressionNode;


public class GodAnalyzer {

	private static MetamodelManager metamodelManager = MetamodelManager.getInstance();

	private static Context context = OperationContextAnalyzer.context;

	private static final String SELF = "self";
	
	public static final String RESULT = "result";

	private static String laterEvaluationOperation;

	private static List<PrimaryExpressionNode> laterEvaluationNodes = new LinkedList<PrimaryExpressionNode>();

	private static boolean hasResult = false;
	
	public static boolean godFlag; 

	public static void setLaterEvaluation(String operation) {
		laterEvaluationOperation = operation;
		godFlag = true;
	}

	public static ExpressionNode check(List<PrimaryExpressionNode> nodes) {
		ExpressionNode firstExpNode = nodes.get(0).getExpressionNode();
		PropertyCallNode firstPropertyCallNode = nodes.get(0).getPropertyCallNode();
		boolean isLiteral = (nodes.size() == 1) && (firstExpNode != null) && (firstPropertyCallNode == null); 
		if (isLiteral) {
			return firstExpNode;
		}
		if (!godFlag) { 
			if (nodes.size() == 1) {
				if (firstPropertyCallNode.getPath().equals(SELF)) {
					return new ExpressionNode(context.getKlass().getType(), null);
				} else {
					Accessorizable element = getVarType(firstPropertyCallNode);
					if (element == null) {
						throw new SemanticException(firstPropertyCallNode.getPath() + " not declared.");
					} else if (element.getAccessor() == "private") {
						throw new SemanticException("Cannot access " + firstPropertyCallNode.getPath() + " cause it's a private element.");
					}
					return new ExpressionNode(element.getType(), null);
				}
			} else {
				return new ExpressionNode(getExpressionType(nodes), null);
			}
		} else {
			laterEvaluationNodes.addAll(nodes);
			return nodes.get(0).getExpressionNode();
		}
	}

	private static String getExpressionType(List<PrimaryExpressionNode> nodes) {
		String currentType = null;
		PropertyCallNode firstPropertyCallNode = nodes.get(0).getPropertyCallNode();
		Accessorizable element = getVarType(firstPropertyCallNode);

		if (firstPropertyCallNode.getPath().equals(SELF)) {
			currentType = context.getKlass().getType();
		} else if (element != null) {
			if (element.getAccessor() == "private") {
				throw new SemanticException("Cannot access " + firstPropertyCallNode.getPath() + " cause it's a private element.");
			} else {
				currentType = element.getType();
			}
		} else {
			throw new SemanticException("'" + firstPropertyCallNode.getPath() + "' not declared");
		}

		PropertyCallNode tempPropertyCallNode;
		for (int i = 1; i < nodes.size(); i++) {
			tempPropertyCallNode = nodes.get(i).getPropertyCallNode();
			if (tempPropertyCallNode.getAccessor() == AccessorTypeEnum.ARROW) {
				currentType = updateCurrentTypeByCollection(currentType, tempPropertyCallNode);
			} else if (tempPropertyCallNode.isMethod()) {
				currentType = updateCurrentTypeByMethod(currentType, tempPropertyCallNode);
			} else {
				currentType = updateCurrentTypeByProperty(currentType, tempPropertyCallNode);
			}
		}

		return currentType;

	}

	private static String updateCurrentTypeByProperty(String currentType, PropertyCallNode tempPropertyCallNode) {
		MyClass klass = metamodelManager.getClass(currentType);
		if (klass == null) {
			throw new SemanticException("Tipo '" + currentType + "' não possui atributo '" + tempPropertyCallNode.getPath() +"'");
		}
		Attribute attribute = klass.getAttribute(tempPropertyCallNode.getPath());
		if (attribute == null) {
			throw new SemanticException("Tipo '" + currentType + "' não possui atributo '" + tempPropertyCallNode.getPath() +"'");
		}
		return attribute.getType();
	}

	private static String updateCurrentTypeByMethod(String currentType,	PropertyCallNode tempPropertyCallNode) {
		if (PrimitiveTypeEnum.isValid(currentType)) {
			throw new SemanticException("Tipo '" + currentType + "' não implementa '" + tempPropertyCallNode.getPath() +"'");
		}

		MyClass klass = metamodelManager.getClass(currentType);
		if (klass == null) {
			throw new SemanticException("Tipo '" + currentType + "' não implementa o método '" + tempPropertyCallNode.getPath() +"'");
		}
		Method method = klass.getMethod(tempPropertyCallNode.getPath());
		if (method == null) {
			throw new SemanticException("Tipo '" + currentType + "' não implementa o método '" + tempPropertyCallNode.getPath() +"'");
		}
		checkParametersOrder(method, tempPropertyCallNode.getParameterList());
		return method.getReturnType();
	}

	private static void checkParametersOrder(Method method, List<ExpressionNode> parameters) {
		List<Attribute> originalParameters = method.getParameters();
		if (originalParameters.size() != parameters.size()) {
			throw new SemanticException("Incorrect number of parameters on method '" + method.getName() + "'");
		}
		for (int i = 0; i < originalParameters.size(); i++) {
			String originalParameterType = originalParameters.get(i).getType();
			String parameterType = parameters.get(i).getType();
			if (!originalParameterType.equals(parameterType)) {
				throw new SemanticException("Incorrect parameters on method '" + method.getName() + "'");
			}
		}
	}

	private static String updateCurrentTypeByCollection(String currentType,	PropertyCallNode tempPropertyCallNode) {
		CollectionAnalyzer.checkIfItsCollection(currentType);
		if (tempPropertyCallNode.getPath().equalsIgnoreCase(laterEvaluationOperation)) {
			Context previousContext = context;
			String insideType = currentType.substring(currentType.indexOf("<")+1, currentType.indexOf(">"));
			context = new Context(metamodelManager.getClass(insideType), null, null);
			godCheck();
			context = previousContext;

		}
		return CollectionAnalyzer.getType(currentType, tempPropertyCallNode);
	}

	private static void godCheck() {
		List<PrimaryExpressionNode> list;
		for (PrimaryExpressionNode node : laterEvaluationNodes) {
			list = new LinkedList<PrimaryExpressionNode>();
			list.add(node);
			ExpressionNode node2 = check(list);
			UndefinedExpressionNode uNode = (UndefinedExpressionNode) node.getExpressionNode();
			if ((NumericExpressionAnalyzer.checkIfItsNumeric(node2.getType()) && !NumericExpressionAnalyzer.checkIfItsNumeric(uNode.getTypeItShouldBe())) 
					|| (!NumericExpressionAnalyzer.checkIfItsNumeric(node2.getType()) && NumericExpressionAnalyzer.checkIfItsNumeric(uNode.getTypeItShouldBe()))) {
				if (!(node2.getType().equalsIgnoreCase(uNode.getTypeItShouldBe()))) {
					throw new SemanticException("Invalid expression inside '" + laterEvaluationOperation + "' operation.");
				}
			}
		}
	}
	
	private static Accessorizable getVarType(PropertyCallNode propertyCallNode) {
		Accessorizable type = null;
		if (context.getParameters() != null) {
			for (Attribute parameter : context.getParameters()) {
				if (parameter.getName().equals(propertyCallNode.getPath())) {
					type = parameter;
				}
			}
		}
		if (type == null) {
			Attribute attribute = context.getKlass().getAttribute(propertyCallNode.getPath());
			if (attribute != null) {
				type = attribute;
			}
		}
		if (type == null) {
			Method method = context.getKlass().getMethod(propertyCallNode.getPath());
			if (method != null) {
				type = method;
				checkParametersOrder(method, propertyCallNode.getParameterList());
			}
		}
		
		return type;
	}
	
	public static void checkBooleanAtPre(ExpressionNode ocl,StereotypeTypeEnum s) throws Exception{
		if(s == StereotypeTypeEnum.PRE && !ocl.getType().equalsIgnoreCase(PrimitiveTypeEnum.BOOLEAN.name())){
			throw new Exception("Era esperada uma expressao booleana na pre-condicao");
		}
	}

	public static void arroz() {
		System.out.println(laterEvaluationNodes);
	}

	public static void checkResult(ExpressionNode e) {
		if(hasResult){
			throw new SemanticException("Não pode haver mais de um Result nas pós-condições.");
		}
		hasResult = true;
		
		if(!context.getMethod().getReturnType().equalsIgnoreCase(e.getType())){
			throw new SemanticException("Expressão declarada no Result não corresponde ao retorno do método. Era esperado " + context.getMethod() + " mas o tipo do post era " + e.getType());
		}
		
	}

}
